using System.Diagnostics;
using System.IO;
using System.Windows;

namespace TwoPageView3
{
    // please refer TIFF 6.0 specification
    class CCITTFaxToTIFF
    {
        enum TiffTag
        {
            NewSubfileType = 254,

            ImageWidth = 256,
            ImageLength = 257,
            BitsPerSample = 258,
            Compression = 259,
            Photometric = 262,

            FillOrder = 266,
            StripOffsets = 273,
            Orientation = 274,
            SamplesPerPixel = 277,
            RowsPerStrip = 278,

            StripByteCounts = 279,
            XResolution = 282,
            YResolution = 283,
            PlanarConfiguration = 284,
            T4Options = 292,

            T6Options = 293,
            ResolutionUnit = 296,
            PageNumber = 297,
            Software = 305,
        }

        enum TiffTagType
        {
            ASCII = 2,
            SHORT = 3,
            LONG = 4,
            RATIONAL = 5,
        }

        private const int HEADER_SIZE = 8;
        private const int IFD_BYTES = 12;
        private const int IFD_NUM = 16;
        private const int RATIONAL_BYTES = 8;
        private const int RATIONAL_NUM = 2;
        private const int IFD_PROLOGUE_BYTES = 2;
        private const int IFD_EPILOGUE_BYTES = 4;

        private CCITTFaxToTIFF() {
        }

        public static int CalcFileSizeFromImageBytes(int imageBytes) {
            uint dataLength = (uint)imageBytes;
            if (0 != (dataLength & 1)) {
                dataLength += 1; //< always even number
            }

            return (int)(HEADER_SIZE + dataLength + IFD_PROLOGUE_BYTES + IFD_BYTES * IFD_NUM
                    + IFD_EPILOGUE_BYTES + RATIONAL_BYTES * RATIONAL_NUM);
        }

        public static byte[] ConvertToTiff(byte[] data, Size wh)
        {
            var stream = new MemoryStream(CalcFileSizeFromImageBytes(data.Length));
            using (var bw = new BinaryWriter(stream)) {

                uint dataLength = (uint)data.Length;
                if (0 != (dataLength & 1)) {
                    dataLength += 1; //< always even number
                }
                WriteTiffHeader(bw, HEADER_SIZE + dataLength);
                bw.Write(data);
                if (0 != (data.Length & 1)) {
                    byte v0 = 0;
                    bw.Write(v0);
                }

                Write2BE(bw, IFD_NUM); //< IFD prologue

                WriteIFDDirectoryEntry(bw, TiffTag.ImageWidth, TiffTagType.SHORT, 1, (uint)wh.Width);
                WriteIFDDirectoryEntry(bw, TiffTag.ImageLength, TiffTagType.SHORT, 1, (uint)wh.Height);
                WriteIFDDirectoryEntry(bw, TiffTag.BitsPerSample, TiffTagType.SHORT, 1, 1);
                WriteIFDDirectoryEntry(bw, TiffTag.Compression, TiffTagType.SHORT, 1, 4); //< 3== CCITT T4, 4==CCITT T6
                WriteIFDDirectoryEntry(bw, TiffTag.Photometric, TiffTagType.SHORT, 1, 0); //< 0==WhiteIsZero, 1==BlackIsZero

                WriteIFDDirectoryEntry(bw, TiffTag.FillOrder, TiffTagType.SHORT, 1, 1);   //< 1== The high-order bit of the first compression code is stored in the high-order bit of byte 0, the next-highest bit of the first compression code is stored in the next-highest bit of byte 0, and so on.
                WriteIFDDirectoryEntry(bw, TiffTag.StripOffsets, TiffTagType.LONG, 1, 8); //< offset to actual element data from the start of the file
                WriteIFDDirectoryEntry(bw, TiffTag.Orientation, TiffTagType.SHORT, 1, 1); //< 1==The 0th row represents the visual top of the element, and the 0th column represents the visual left-hand side.
                WriteIFDDirectoryEntry(bw, TiffTag.SamplesPerPixel, TiffTagType.SHORT, 1, 1);
                WriteIFDDirectoryEntry(bw, TiffTag.RowsPerStrip, TiffTagType.LONG, 1, 0xffffffff);

                WriteIFDDirectoryEntry(bw, TiffTag.StripByteCounts, TiffTagType.LONG, 1, (uint)data.Length);
                WriteIFDDirectoryEntry(bw, TiffTag.XResolution, TiffTagType.RATIONAL, 1,
                        (uint)(HEADER_SIZE + dataLength + IFD_PROLOGUE_BYTES + IFD_BYTES * IFD_NUM + IFD_EPILOGUE_BYTES));
                WriteIFDDirectoryEntry(bw, TiffTag.YResolution, TiffTagType.RATIONAL, 1,
                        (uint)(HEADER_SIZE + dataLength + IFD_PROLOGUE_BYTES + IFD_BYTES * IFD_NUM + IFD_EPILOGUE_BYTES + RATIONAL_BYTES));
                WriteIFDDirectoryEntry(bw, TiffTag.PlanarConfiguration, TiffTagType.SHORT, 1, 1);
                WriteIFDDirectoryEntry(bw, TiffTag.T6Options, TiffTagType.LONG, 1, 0); // bit0: unused, bit1: uncompressed/compressed

                WriteIFDDirectoryEntry(bw, TiffTag.ResolutionUnit, TiffTagType.SHORT, 1, 2); //< Inch

                Write4BE(bw, 0);

                Write4BE(bw, 72); //< XResolution. offset == headerSize + dataLength + 2 + 12 * nIFD + 4
                Write4BE(bw, 1);
                Write4BE(bw, 72); //< YResolution.
                Write4BE(bw, 1);
            }
            stream.Close();
            return stream.GetBuffer();
        }

        private static void WriteTiffHeader(BinaryWriter bw, uint directoryEntryOffset)
        {
            Write2BE(bw, 0x4d4d); //< 0x4d4d==big endian, 0x4949==little endian
            Write2BE(bw, 42);     //< TIFF magic number
            Write4BE(bw, directoryEntryOffset);
        }

        private static void WriteIFDDirectoryEntry(BinaryWriter bw, TiffTag tag, TiffTagType type, uint count, uint val)
        {
            Write2BE(bw, (ushort)tag);   // 0 - 1
            Write2BE(bw, (ushort)type);  // 2 - 3
            Write4BE(bw, count); // 4 - 7
            switch (type) {
            case TiffTagType.SHORT:
                if (1 == count) {
                    Write2BE(bw, (ushort)val);
                    Write2BE(bw, 0);
                } else if (2 == count) {
                    Write4BE(bw, val);
                } else {
                    Debug.Assert(false);
                }
                break;
            case TiffTagType.LONG:
                if (1 == count) {
                    Write4BE(bw, val);
                } else {
                    Debug.Assert(false);
                }
                break;
            case TiffTagType.RATIONAL:
                if (1 == count) {
                    Write4BE(bw, val);
                } else {
                    Debug.Assert(false);
                }
                break;
            case TiffTagType.ASCII:
                if (count < 3) {
                    Debug.Assert(false);
                }
                Write4BE(bw, val);
                break;
            default:
                Debug.Assert(false);
                break;
            }
        }

        private static void Write2BE(BinaryWriter bw, ushort v)
        {
            byte x;
            x = (byte)((v >> 8) & 0xff);
            bw.Write(x);
            x = (byte)(v & 0xff);
            bw.Write(x);
        }

        private static void Write4BE(BinaryWriter bw, uint v)
        {
            byte x;
            x = (byte)((v >> 24) & 0xff);
            bw.Write(x);
            x = (byte)((v >> 16) & 0xff);
            bw.Write(x);
            x = (byte)((v >> 8) & 0xff);
            bw.Write(x);
            x = (byte)(v & 0xff);
            bw.Write(x);
        }
    }
}
